iT邦幫忙

2022 iThome 鐵人賽

DAY 22
0

昨天我們看完了文章總覽頁
今天我們進到文章內頁

程式碼如下:

import { Icon } from '@chakra-ui/icons'
// ~中間略~

const messages = defineMessages({
  prevPost: { id: 'blog.common.prevPost', defaultMessage: '上一則' },
  nextPost: { id: 'blog.common.nextPost', defaultMessage: '下一則' },
  blogSuggestion: { id: 'blog.label.blogSuggestion', defaultMessage: '回應' },
})

const StyledTitle = styled.div`
  // ~中間略~
`
const StyledTag = styled.span`
  // ~中間略~
`
const StyledLabel = styled.div`
  // ~中間略~
`
const StyledSubTitle = styled.div`
  // ~中間略~
`

const StyledPostTitle = styled.h3`
  // ~中間略~
`

const BlogPostPage: React.VFC = () => {
  const { formatMessage } = useIntl()
  const { currentMemberId } = useAuth()
  const { searchId } = useParams<{ searchId: string }>()
  const app = useApp()
  const { loadingPost, post, refetchPosts } = usePost(searchId)
  const postId = post?.id
  const addPostView = useAddPostViews()
  const { insertPostReaction, deletePostReaction } = useMutatePostReaction(postId)

  const [isScrollingDown, setIsScrollingDown] = useState(false)
  const [isLiked, setIsLiked] = useState(false)

  const handleGetPostLikes = () => {
    const postLikesData: { postId: string }[] = JSON.parse(localStorage.getItem('kolabe.post_reaction') || '[]')
    const isThisPostLikes: boolean = postLikesData.some(v => v.postId === postId)
    setIsLiked(isThisPostLikes)
  }

  useEffect(() => {
    document.getElementById('layout-content')?.scrollTo({ top: 0 })
    handleGetPostLikes()
  }, [postId])

  const handleScroll = useCallback(
    throttle(() => {
      const postCoverElem = document.querySelector('#post-cover')
      const layoutContentElem = document.querySelector('#layout-content')
      if (!postCoverElem || !layoutContentElem) {
        return
      }

      if (layoutContentElem.scrollTop > postCoverElem.scrollHeight) {
        if (isScrollingDown) {
          return
        }
        setIsScrollingDown(true)
      } else {
        setIsScrollingDown(false)
      }
    }, 100),
    [post],
  )

  useEffect(() => {
    const layoutContentElem = document.querySelector('#layout-content')
    if (!layoutContentElem) {
      return
    }

    layoutContentElem.addEventListener('scroll', () => handleScroll())
    return layoutContentElem.removeEventListener('scroll', () => handleScroll())
  }, [handleScroll])

  if (!app.loading && !app.enabledModules.blog) {
    return <ForbiddenPage />
  }
  if (loadingPost) {
    return <LoadingPage />
  }
  if (!post) {
    return <NotFoundPage />
  }

  try {
    const visitedPosts = JSON.parse(sessionStorage.getItem('kolable.posts.visited') || '[]') as string[]
    if (!visitedPosts.includes(post.id)) {
      visitedPosts.push(post.id)
      sessionStorage.setItem('kolable.posts.visited', JSON.stringify(visitedPosts))
      addPostView(post.id)
    }
  } catch (error) {}

  const handleLikeStatus = async () => {
    if (isLiked) {
      await deletePostReaction()
      setIsLiked(false)
    } else {
      await insertPostReaction()
      setIsLiked(true)
    }
    await refetchPosts()
  }
  return (
    <DefaultLayout white noHeader={isScrollingDown}>
      <BlogPostPageHelmet post={post} />

      <div className="container py-sm-5">
        <div className="row justify-content-center">
          <div className="col-12 col-lg-9">
            {!loadingPost && (
              <PostCover
                title={post?.title || ''}
                coverUrl={post?.videoUrl || post?.coverUrl || null}
                type={post?.videoUrl ? 'video' : 'picture'}
                merchandises={post?.merchandises || []}
                isScrollingDown={isScrollingDown}
              />
            )}
            <StyledPostMeta className="pb-3">
              <Icon as={UserOIcon} className="mr-1" />
              <span className="mr-2">{post?.author.name}</span>
              <Icon as={CalendarAltOIcon} className="mr-1" />
              <span className="mr-2">{post?.publishedAt ? moment(post.publishedAt).format('YYYY-MM-DD') : ''}</span>
              <Icon as={EyeIcon} className="mr-1" />
              <span>{post?.views}</span>
            </StyledPostMeta>
            <StyledTitle>{post?.title}</StyledTitle>
            <StyledPostMeta className="pb-3">{post.source}</StyledPostMeta>
            <div className="mb-5">
              {loadingPost ? (
                <SkeletonText mt="1" noOfLines={4} spacing="4" />
              ) : (
                <BraftContent>{post?.description}</BraftContent>
              )}
            </div>
            <div className="row mb-5">
              <div className="col-6 col-lg-4">
                {post?.tags.map(tag => (
                  <Link key={tag} to={`/posts/?tags=${tag}`} className="mr-2">
                    <StyledTag>#{tag}</StyledTag>
                  </Link>
                ))}
              </div>
              <div className="col-6 col-lg-4 offset-lg-4  d-flex align-items-center justify-content-end">
                <SocialSharePopover url={window.location.href} />
                <LikesCountButton onClick={handleLikeStatus} count={post.reactedMemberIdsCount} isLiked={isLiked} />
              </div>
            </div>
            <Divider className="mb-3" />
            <div className="py-3">
              {post?.author && (
                <CreatorCard
                  id={post.author.id}
                  avatarUrl={post.author.avatarUrl}
                  title={post.author.name}
                  labels={[]}
                  description={post.author.abstract || ''}
                  withProgram
                  withPodcast
                  withAppointment
                  withBlog
                  noPadding
                />
              )}
            </div>
            <Divider className="mb-5" />
            <div className="row mb-5">
              <div className="col-6 col-lg-4">
                {post?.prevPost && (
                  <Link to={`/posts/${post.prevPost.codeName || post.prevPost.id}`}>
                    <StyledLabel>{formatMessage(messages.prevPost)}</StyledLabel>
                    <StyledSubTitle>{post.prevPost.title}</StyledSubTitle>
                  </Link>
                )}
              </div>
              <div className="col-6 col-lg-4 offset-lg-4">
                {post?.nextPost && (
                  <Link to={`/posts/${post.nextPost.codeName || post.nextPost.id}`} className="text-right">
                    <StyledLabel>{formatMessage(messages.nextPost)}</StyledLabel>
                    <StyledSubTitle>{post.nextPost.title}</StyledSubTitle>
                  </Link>
                )}
              </div>
            </div>
            <div className="row">{postId && <RelativePostCollection postId={postId} tags={post?.tags} />}</div>
            <div className="mb-4">
              <StyledPostTitle className="mb-3">{formatMessage(messages.blogSuggestion)}</StyledPostTitle>
              {currentMemberId && (
                <SuggestionCreationModal threadId={`/posts/${postId}`} onRefetch={() => refetchPosts()} />
              )}
              {post?.suggests.map(v => (
                <div key={v.id}>
                  <MessageSuggestItem
                    key={v.id}
                    suggestId={v.id}
                    memberId={v.memberId}
                    description={v.description}
                    suggestReplyCount={v.suggestReplyCount}
                    programRoles={post?.postRoles || []}
                    reactedMemberIds={v.reactedMemberIds}
                    createdAt={v.createdAt}
                    onRefetch={() => refetchPosts()}
                  />
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
    </DefaultLayout>
  )
}

export default BlogPostPage

文章 Banner、標題和內文

裡面也分成了很多 Component
在元件最上方有一個「BlogPostPageHelmet」
是專門 for SEO 和 OpeGraph 的設定

接著就是文章本體
首先最上方是文章的 Banner
下芳則是有關文章的 Meta Data
再來就是文章本身
它是由「BraftContent」這個 Component
解析從後台儲存的編輯器文字,轉成 HTML 的格式呈現

文章標籤、作者、相關文章和回覆

文章的最下方顯示這個文章有的標籤
點擊後會找尋這個標籤相關的文章
使用 react-router-dom 的 Link 完成

旁邊有分享和按讚的按鈕,分別使用「SocialSharePopover」和「LikesCountButton」

再來是發文者的資訊
他是使用「CreatorCard」這個元件完成

最後則是文章相關的區塊
分別是看上一篇和下一篇文章
以及相關的文章,由「RelativePostCollection」完成
帶入指定的 Tag,就會返回相關的文章

最後則是回覆的功能,這塊涵蓋了許多元件,就省略不講

明天我們來看探索課程的部分


上一篇
Blog (1)
下一篇
Program (1)
系列文
從 Open Source 專案學習 React 開發 - 以 lodestar-app 為例30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言